package de.sendorian.app.forumArchive;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.hamcrest.Matcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import de.sendorian.app.forumArchive.domain.Forum;
import de.sendorian.app.forumArchive.domain.Topic;
import de.sendorian.app.forumArchive.repository.ForumRepository;


/**
 * Compresses PNG images with PNGout to save space with lossless optimizations.
 * 
 * @author sendorian
 * 
 */
@Component
public class Compressor implements Runnable {

    private static Logger LOG = Logger.getLogger(Compressor.class);

    private static final String PNGOUT = new File("bin/pngout").getAbsolutePath();
    private static final String os = System.getProperty("os.name");
    private Matcher<Topic> topicMatcher;
    private Collector collector;
    private ForumRepository forumRepository;


    @Override
    public void run() {
        compressImages(forumRepository.findAll());
    }


    /**
     * Compresses all images in the topic folders that matches the filter. This
     * is a workaround because it is not possible to get informations for the
     * images which were contained in an archive. So simply all images in that
     * folder will be compressed.
     * 
     */
    public void compressImages(List<Forum> forums) {
        Set<String> allImages = collector.collectImagesInForumDirs(forums, topicMatcher);
        LOG.info("All images collected. Compressing " + allImages.size() + " images");
        compressImages(allImages);
    }


    public void compressImagesInFolders(Set<String> dirs) {
        Set<String> allFiles = collector.collectImagesInFolders(dirs);
        LOG.info("All images collected. Compressing " + allFiles.size() + " images");
        compressImages(allFiles);
    }


    /**
     * Compresses all the images in the Set. (The Images have to have a fully
     * qualified path!)
     * 
     */
    public void compressImages(Set<String> images) {
        if (!isOperatingSystemSupported()) {
            LOG.warn("Compression is not supported on this operating system. Skipping...");
            return;
        }

        int processors = Runtime.getRuntime().availableProcessors();
        LOG.info("Using " + processors + " threads to compress images.");
        ExecutorService threadPool = Executors.newFixedThreadPool(processors);

        int processed = 1;
        for (String image1 : images) {
            Runnable compressionTask = createCompressionTask(image1, processed, images.size());
            threadPool.execute(compressionTask);
            processed++;
        }

        waitToComplete(threadPool);
    }


    /**
     * Creates a Runnable that compresses the image.
     */
    protected Runnable createCompressionTask(String image1, int processed, int totalCount) {
        return new CompressionTask(image1, processed, totalCount);
    }


    /**
     * Waits until all tasks have been executed.
     */
    private void waitToComplete(ExecutorService threadPool) {
        LOG.info("All compression tasks have been initiated. Waiting for tasks to complete.");
        threadPool.shutdown();
        try {
            threadPool.awaitTermination(120 * 60, TimeUnit.SECONDS);
            LOG.info("All compression tasks have been completed.");
        } catch (InterruptedException e) {
            LOG.error("Compressor was interrupted!", e);
        }
    }


    /**
     * Checks if the current operating system is supported, i.e. if a pngout
     * implementation is present.
     */
    private boolean isOperatingSystemSupported() {
        return os.equals("Mac OS X");
    }


    @Autowired
    public void setForumRepository(ForumRepository forumRepository) {
        this.forumRepository = forumRepository;
    }


    @Value("#{config.topicMatcher}")
    public void setTopicMatcher(Matcher<Topic> topicMatcher) {
        this.topicMatcher = topicMatcher;
    }


    @Autowired
    public void setCollector(Collector collector) {
        this.collector = collector;
    }

    private static final class CompressionTask implements Runnable {

        String fileName;
        int processed;
        int totalCount;


        CompressionTask(String fileName, int processed, int totalCount) {
            this.fileName = fileName;
            this.processed = processed;
            this.totalCount = totalCount;
        }


        public void run() {
            Process proc = null;
            try {
                long begin = System.currentTimeMillis();
                proc = startProcess(fileName);
                endProcess(proc, fileName, begin);
                LOG.info("Processed image " + processed + " of " + totalCount);
            } catch (Exception e) {
                LOG.error("Could not execute pngout!", e);
            } finally {
                close(proc);
            }
        }


        private Process startProcess(String fileName) throws IOException {
            LOG.debug("Compressing \"" + fileName + "\"");
            return new ProcessBuilder(PNGOUT, fileName).start();
        }


        private void endProcess(Process proc, String fileName, long begin) throws InterruptedException {
            proc.waitFor();
            if (proc.exitValue() != 0) {
                LOG.error("Error executing PNGOUT on " + fileName);
            } else {
                long time = System.currentTimeMillis() - begin;
                BigDecimal timeInSeconds = new BigDecimal(time / 1000).setScale(1, BigDecimal.ROUND_HALF_UP);
                LOG.debug("Finished Compressing \"" + fileName + "\" in " + timeInSeconds + " [s]");
            }
        }


        private void close(Closeable c) {
            if (c != null) {
                try {
                    c.close();
                } catch (IOException e) {
                    // ignored
                }
            }
        }


        private void close(Process proc) {
            if (proc != null) {
                close(proc.getOutputStream());
                close(proc.getInputStream());
                close(proc.getErrorStream());
                proc.destroy();
            }
        }
    }
}
